Skip to content

15. 安全鉴权

📝 模块更新日志 新特性*

+ 新增 授权失败可以设置 `Http` 状态码 `context.Fail(statusCode)` 4\.9\.3\.14 ⏱️2024\.05\.14 [542eb8c](https://gitee.com/dotnetchina/Furion/commit/542eb8c93eea1d2178178367199556f19717d830)
+ 新增 `JWT` 授权配置 `RequireExpirationTime` 属性,解决 `JWT` 过期时间不能大于 `13年` 问题 4\.9\.1\.46 ⏱️2024\.03\.13 [\#I9840M](https://gitee.com/dotnetchina/Furion/issues/I9840M)
  • 突破性变化

    • 调整 授权处理程序 AppAuthorizeHandler 接口的 HandleAsync 方法签名,新增 DefaultHttpContext 参数 4.9.3 ⏱️2024.05.10 52d3c2c edc51f4

查看变化修改了 AppAuthorizeHandler 中的 HandleAsync 方法签名,增加了一个 DefaultHttpContext 类型的参数 httpContext

- public override Task HandleAsync(AuthorizationHandlerContext context)  
+ public override Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)  

  • 问题修复

    • 修复 客户端设置 JWT Token 时如果 Bearer 后面跟多个空格导致验证失败问题 4.9.2.8 ⏱️2024.04.08 @xuejf168 !874
    • 修复 不注册 AddJwt 不能使用 JWTEncryption.Encrypt 方法问题 4.9.1.53 ⏱️2024.03.16 5882cf9
    • 修复 使用刷新 Token 也能通过鉴权检查严重安全 Bug 4.8.8.42 ⏱️2023.08.28 #I7TII4

15.1 什么是鉴权

鉴权实际上就是一种身份认证

由用户提供凭据,然后将其与存储在操作系统、数据库、应用或资源中的凭据进行比较。 在授权过程中,如果凭据匹配,则用户身份验证成功,可执行已向其授权的操作。 授权指判断允许用户执行的操作的过程。 也可以将身份验证理解为进入空间(例如服务器、数据库、应用或资源)的一种方式,而授权是用户可以对该空间(服务器、数据库或应用)内的哪些对象执行哪些操作。

15.1.1 常见的鉴权方式

  • HTTP Basic Authentication

这是 HTTP 协议实现的基本认证方式,我们在浏览网页时,从浏览器正上方弹出的对话框要求我们输入账号密码,正是使用了这种认证方式

  • Session + Cookie

利用服务器端的 session(会话)和浏览器端的 cookie 来实现前后端的认证,由于 http 请求时是无状态的,服务器正常情况下是不知道当前请求之前有没有来过,这个时候我们如果要记录状态,就需要在服务器端创建一个会话(session),将同一个客户端的请求都维护在各自的会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端创建 session,如果有则已经认证成功了,否则就没有认证。

  • Token

客户端在首次登录以后,服务端再次接收 HTTP 请求的时候,就只认 Token 了,请求只要每次把 Token 带上就行了,服务器端会拦截所有的请求,然后校验 Token 的合法性,合法就放行,不合法就返回 401(鉴权失败)

Token验证比较灵活,适用于大部分场景。常用的 Token 鉴权方式的解决方案是 JWTJWT 是通过对带有相关用户信息的进行加密,加密的方式比较灵活,可以根据需求具体设计。

  • OAuth

OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供 OAuth 认证服务的厂商有支付宝、QQ 和微信。

OAuth 协议又有 1.0 和 2.0 两个版本。相比较 1.0 版,2.0 版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。

15.2 如何使用

配置之前在添加授权服务之前,请先确保 Startup.csConfigure 是否添加了以下两个中间件:

app.UseAuthentication();  
app.UseAuthorization();  

使用说明如果您使用的是 WebAPI,则该小节可忽略,通常 WebAPI 使用的是 JWT 授权方式,而非 Cookie

// Cookies单独身份验证  
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)  
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, b =>  
        {  
            b.LoginPath = "/Home/Login";  
        });  

15.2.2 添加 Jwt 身份验证

  • 安装 Furion.Extras.Authentication.JwtBearer 拓展包
  • Startup.cs 中注册 AddJwt 服务,注意,必须在 .AddControllers() 之前注册!!
// 默认授权机制,需授权的即可(方法)需贴 `[Authorize]` 特性  
services.AddJwt();  

// 启用全局授权,这样每个接口都必须授权才能访问,无需贴 `[Authorize]` 特性,推荐!!!!!!!!!❤️  
// services.AddJwt<JwtHandler>(enableGlobalAuthorize:true);  

注:如果项目使用了 services.AddSignalR(); 服务,那么该服务必须在 services.AddJwt 之后注册。

额外补充默认 JwtHandler 代码:

using Furion.Authorization;  
using Microsoft.AspNetCore.Authorization;  
using Microsoft.AspNetCore.Http;  
using System.Threading.Tasks;  

namespace FurionApi.Web.Core;  

public class JwtHandler : AppAuthorizeHandler  
{  
    public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)  
    {  
        // 这里写您的授权判断逻辑,授权通过返回 true,否则返回 false  
        // 默认授权失败返回状态码为 403,如需自定义状态码可通过 context.StatusCode(状态码) 或 context.Fail(状态码) 设置,并结合 `app.UseUnifyResultStatusCodes()` 使用,Furion 4.9.3.14+ 支持  

        return Task.FromResult(true);  
    }  
}  

  • 自定义 Jwt 配置(默认无需配置
{  
  "JWTSettings": {  
    "ValidateIssuerSigningKey": true, // 是否验证密钥,bool 类型,默认true  
    "IssuerSigningKey": "你的密钥", // 密钥,string 类型,必须是复杂密钥,长度大于16,.NET8+ 长度需大于 32  
    "ValidateIssuer": true, // 是否验证签发方,bool 类型,默认true  
    "ValidIssuer": "签发方", // 签发方,string 类型  
    "ValidateAudience": true, // 是否验证签收方,bool 类型,默认true  
    "ValidAudience": "签收方", // 签收方,string 类型  
    "ValidateLifetime": true, // 是否验证过期时间,bool 类型,默认true,建议true  
    "ExpiredTime": 20, // 过期时间,long 类型,单位分钟,默认20分钟,最大支持 13 年  
    "ClockSkew": 5, // 过期时间容错值,long 类型,单位秒,默认 5秒  
    "Algorithm": "HS256", // 加密算法,string 类型,默认 HS256  
    "RequireExpirationTime": true // 验证过期时间,设置 false 将永不过期,Furion 4.9.1.46+ 支持  
  }  
}  

系统安全注意事项Furion 框架为了方便开发,已经自动添加了 Jwt 默认配置。建议每个项目都应该单独配置 IssuerSigningKeyValidIssuerValidAudience 这三个。否则同样用了 Furion 框架生成的 Token 可能存在相互访问各自系统的风险。

Algorithm 算法支持列表目前支持的加密算法

  • HS256
  • HS384
  • HS512
  • PS256
  • PS384
  • PS512
  • RS256:需自行实现算法
  • RS384:需自行实现算法
  • RS512:需自行实现算法
  • ES256
  • ES256K
  • ES384
  • ES512
  • EdDSA

详情请查阅 SecurityAlgorithms

  • ❤️ ❤️ 生成 Token

通常我们需要在登录成功之后生成 JWT Token 并返回,可通过 JWTEncryption.Encrypt 静态方法生成,如:

关于 Token 的值字典 Dictionary 中的值支持所有基元类型和基元类型组成的值,但应尽可能避免使用 数组 值。

// 生成 token  
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()  
            {  
                { "UserId", user.Id },  // 存储Id  
                { "Account",user.Account }, // 存储用户名  
            });  

15.2.3 混合身份验证

有时候我们一个系统中需要多种混合验证方式,这时候我们需要配置一个主验证 方式,所以需要修改 options.DefaultAuthenticateSchemeoptions.DefaultChallengeScheme 为主验证方式。

如需第二种方式,只需要通过 [Authorize(JwtBearerDefaults.AuthenticationScheme)] 贴即可。

  • JWTCookies 混合身份验证
services.AddJwt<JwtHandler>(options =>  
{  
      options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;    // 更改默认验证为 Cookies  
      options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;   // 更改默认验证为 Cookies  
})  
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>  
{  
       options.LoginPath = "/Home/Login";  
});  

  • JWTWindows 身份验证混合验证
services.AddJwt<JwtHandler>(options =>  
{  
    options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme; // 更改默认验证为 Windows 身份验证  
    options.DefaultChallengeScheme = NegotiateDefaults.AuthenticationScheme;    // 更改默认验证为 Windows 身份验证  
})  
.AddNegotiate();  

  • 应用例子
// 贴了 [Authorize] 则表示应用 JwtBearerDefaults.AuthenticationScheme  
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]  
public class ApiServices : IDynamicApiController  
{  
}  

// 不贴则应用主验证,也即是 `DefaultAuthenticateScheme` 设置的  
public class HomeController: Controller  
{  
}  

15.2.4 自定义策略授权

在一些复杂的应用系统中,可能存在多套授权机制,比如特定区域使用 JWT 授权,MVC 架构项目使用 Cookies 授权,还有一些特殊的接口使用自定义授权。这时候我们可以通过自定义策略授权方式:

  1. 自定义授权需求类和处理器
public class CustomAuthorizationRequirement : IAuthorizationRequirement  
{  
    // 可以在此处定义任何你需要验证的信息或逻辑  
}  

public class CustomAuthorizationHandler : AuthorizationHandler<CustomAuthorizationRequirement>  
{  
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement)  
    {  
        // 实现你的自定义验证逻辑  

        if (/* 验证条件满足 */)  
        {  
            context.Succeed(requirement);  
        }  

        return Task.CompletedTask;  
    }  
}  

  1. 注册自定义授权处理器
services.AddAuthorization(options =>  
{  
    options.AddPolicy("CustomPolicy", policy =>  
    {  
        policy.Requirements.Add(new CustomAuthorizationRequirement());  
    });  

    // 添加自定义授权处理器  
    services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();  
});  

  1. 添加自定义授权策略 [Authorize(Policy = "CustomPolicy")]
[ApiController]  
public class MyController : ControllerBase  
{  
    [HttpGet("/api/some-endpoint")]  
    [Authorize(Policy = "CustomPolicy")] // 使用自定义授权策略  
    public IActionResult SomeEndpoint()  
    {  
        // ...  
    }  

    // 其他 API 端点仍受 JWT 验证保护  
    [HttpGet("/api/protected-endpoint")]  
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] // 此处继续使用JWT验证  
    public IActionResult ProtectedEndpoint()  
    {  
        // ...  
    }  
}  

15.2.5 Basic 授权(HTTP 基本认证)

HTTP 中,HTTP 基本认证(Basic Authentication)是一种允许网页浏览器或其他客户端程序以(用户名:口令密码)请求资源的身份验证方式。如需配置 Basic 授权可参考以下配置:

  1. 创建 Basic 验证处理程序
namespace YourProject.Core;  

public class BasicAuthenticationDefaults  
{  
    public const string AuthenticationScheme = "BasicAuthentication";  
}  

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>  
{  
    private readonly IMyUserService _userService;  

    public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options  
        , ILoggerFactory logger  
        , UrlEncoder encoder  
        , IMyUserService userService)   // 依赖注入你的用户服务或者用户表仓储  
        : base(options, logger, encoder)  
    {  
        _userService = userService;  
    }  

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()  
    {  
        // 检查请求报文头是否包含 Authorization 头,且值以 (Basic )开头  
        if (!Context.Request.Headers.TryGetValue("Authorization", out StringValues authorizationHeader)  
            || !authorizationHeader.ToString().StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))  
        {  
            return AuthenticateResult.NoResult();  
        }  

        // 读取请求报文头 Authorization 的信息并进行 Base64 解码  
        var token = authorizationHeader.ToString()[6..].Trim();  
        var credentialString = Encoding.UTF8.GetString(Convert.FromBase64String(token));  

        // 通过 : 分隔符分割出用户名和密码  
        var credentials = credentialString.Split(':');  
        string username = credentials[0];  
        string password = credentials[1];  

        // ========================== 验证你的用户名密码(成功)==========================  
        if (await _userService.ValidateCredentials(username, password))  
        {  
            // 验证成功写入授权上下文票据  
            var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };  
            var identity = new ClaimsIdentity(claims, Scheme.Name);  
            var principal = new ClaimsPrincipal(identity);  
            var ticket = new AuthenticationTicket(principal, null, Scheme.Name);  

            return AuthenticateResult.Success(ticket);  
        }  

        // 验证失败返回  
        return AuthenticateResult.Fail("Invalid username or password.");  
    }  
}  

IMyUserService 说明IMyUserService 在这里只是一个实现验证用户名和密码的逻辑服务例。你可以是注入仓储服务,如 IRepository<User>,也可以是任何可以认证用户名和密码的服务。这里给出一个基本的例子:

public interface IMyUserService  
{  
    Task<bool> ValidateCredentials(string username, string password);  
}  

public class MyUserService : IMyUserService, IScoped  
{  
    public Task<bool> ValidateCredentials(string username, string password)  
    {  
        // 这里验证用户名和密码。。。  
    }  
}  

  1. 注册 BasicAuthenticationHandler
services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)  
    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(BasicAuthenticationDefaults.AuthenticationScheme  
    , configureOptions =>  
    {  
        // 更多配置  
    });  

  1. 使用 Basic 授权

  2. 全局授权

services.Configure<MvcOptions>(options =>  
{  
    options.Filters.Add(new AuthorizeFilter());  
});  

  • 局部授权
[Authorize]  
public class AdminController : Controller  
{  
    // ...  
}  

  • 多套授权中使用
[Authorize(AuthenticationSchemes = BasicAuthenticationDefaults.AuthenticationScheme)]  
public class AdminController : Controller  
{  
    // ...  
}  

  • Swagger 中启用 Basic 授权
{  
  "SpecificationDocumentSettings": {  
    // ...其他配置  

    "SecurityDefinitions": [  
      {  
        "Id": "Bearer", // JWT 授权  
        "Type": "Http",  
        "Name": "Authorization",  
        "Description": "JWT Authorization header using the Bearer scheme.",  
        "BearerFormat": "JWT",  
        "Scheme": "bearer",  
        "In": "Header",  

        "Requirement": {  
          "Scheme": {  
            "Reference": {  
              "Id": "Bearer",  
              "Type": "SecurityScheme"  
            },  
            "Accesses": []  
          }  
        }  
      },  

      {  
        "Id": "basic", // Basic 授权  
        "Type": "Http",  
        "Name": "basic",  
        "Description": "Basic Authorization header using the username and password.",  
        "Scheme": "basic",  
        "In": "Header",  

        "Requirement": {  
          "Scheme": {  
            "Reference": {  
              "Id": "basic",  
              "Type": "SecurityScheme"  
            },  
            "Accesses": []  
          }  
        }  
      }  
    ]  
  }  
}  

* 客户端(JavaScript)如何设置授权信息

// 将用户名和密码组合成一个字符串,用冒号分隔  
var authString = username + ':' + password;  

// 对组合后的字符串进行Base64编码  
var encodedAuthString = btoa(authString);  

之后在发送请求时在 Headers 中添加,如:

headers: {  
    'Content-Type': 'application/json',  
    'Authorization': 'Basic ' + encodedAuthString // 添加基本认证头  
}  

15.3 高级自定义授权

Furion 框架提供了非常灵活的高级策略鉴权和授权方式,通过该策略授权方式可以实现任何自定义授权。

15.3.1 AppAuthorizeHandler

Furion 框架提供了 AppAuthorizeHandler 策略授权处理程序提供基类,只需要创建自己的 Handler 继承它即可。如:JwtHandler

using Furion.Authorization;  
using Furion.Core;  
using Microsoft.AspNetCore.Authorization;  
using Microsoft.AspNetCore.Http;  
using Microsoft.IdentityModel.JsonWebTokens;  

namespace Furion.Web.Core  
{  
    /// <summary>  
    /// JWT 授权自定义处理程序  
    /// </summary>  
    public class JwtHandler : AppAuthorizeHandler  
    {  
        /// <summary>  
        /// 请求管道  
        /// </summary>  
        /// <param name="context"></param>  
        /// <param name="httpContext"></param>  
        /// <returns></returns>  
        public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)  
        {  
            // 此处已经自动验证 Jwt token的有效性了,无需手动验证  

            // 检查权限,如果方法是异步的就不用 Task.FromResult 包裹,直接使用 async/await 即可  
            return Task.FromResult(CheckAuthorzie(httpContext));  
        }  

        /// <summary>  
        /// 检查权限  
        /// </summary>  
        /// <param name="httpContext"></param>  
        /// <returns></returns>  
        private static bool CheckAuthorzie(DefaultHttpContext httpContext)  
        {  
            // 获取权限特性  
            var securityDefineAttribute = httpContext.GetMetadata<SecurityDefineAttribute>();  
            if (securityDefineAttribute == null) return true;  

            return "查询数据库返回是否有权限";  
        }  
    }  
}  

之后注册 JwtHandler 即可:

services.AddJwt<JwtHandler>();  

15.3.2 完全自定义授权

有些时候可能针对不同的平台采用不一样的授权方式,比如合作信任的第三方机构可以免授权,这时候我们只需要重写 HandleAsync 方法即可。如:

using Furion.Authorization;  
using Furion.Core;  
using Microsoft.AspNetCore.Authorization;  
using Microsoft.AspNetCore.Http;  
using System.Threading.Tasks;  

namespace Furion.Web.Core  
{  
    public class JwtHandler : AppAuthorizeHandler  
    {  
        // public override async Task HandleAsync(AuthorizationHandlerContext context)  // Furion 4.9.3 之前版本使用这个  
        public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)  
        {  
            // 常规授权(可以判断是不是第三方)  
            var isAuthenticated = context.User.Identity.IsAuthenticated;  

            // 第三方授权自定义  
            if (是第三方)  
            {  
                foreach (var requirement in pendingRequirements)  
                {  
                    // 授权成功  
                   context.Succeed(requirement);  
                }  
            }  
            // 授权失败  
            else context.Fail();  
        }  
    }  
}  

15.4 授权特性及全局授权

默认情况下,所有的路由都是允许匿名访问的,所以如果需要对某个 ActionController 设定授权访问,只需要在 ActionController[AppAuthorize][Authorize] 特性即可。

如果需要对特定的 ActionController 允许匿名访问,则贴 [AllowAnonymous] 即可。

15.4.1 全局授权

services.AddJwt<JwtHandler>(enableGlobalAuthorize:true);  

15.4.2 匿名访问

如果需要对特定的 ActionController 允许匿名访问,则贴 [AllowAnonymous] 即可。

15.5 自动刷新 Token

15.5.1 后端登录部分

当用户登录成功之后,返回 accessToken 字符串,之后通过 JWTEncryption.GenerateRefreshToken() 获取 刷新Token,并通过响应报文头返回,如:

// token  
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()  
            {  
                { "UserId", user.Id },  // 存储Id  
                { "Account",user.Account }, // 存储用户名  
            });  

// 获取刷新 token  
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, 43200); // 第二个参数是刷新 token 的有效期(分钟),默认三十天,最大支持13年  

// 设置响应报文头  
httpContextAccessor.HttpContext.Response.Headers["access-token"] = accessToken;  
httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;  

用户登录成功之后把 accessTokenrefreshToken 一起返回给客户端存储起来。

15.5.2 后端授权 Handler 部分

using Furion.Authorization;  
using Furion.Core;  
using Furion.DataEncryption;  
using Microsoft.AspNetCore.Authorization;  
using Microsoft.AspNetCore.Http;  
using Microsoft.Extensions.DependencyInjection;  
using System.Threading.Tasks;  

namespace Furion.Web.Core  
{  
    /// <summary>  
    /// JWT 授权自定义处理程序  
    /// </summary>  
    public class JwtHandler : AppAuthorizeHandler  
    {  
        /// <summary>  
        /// 重写 Handler 添加自动刷新授权逻辑  
        /// </summary>  
        /// <param name="context"></param>  
        /// <returns></returns>  
        // public override async Task HandleAsync(AuthorizationHandlerContext context)  // Furion 4.9.3 之前版本使用这个  
        public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)  
        {  
            // 自动刷新 token  
            if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext()))  
            {  
                await AuthorizeHandleAsync(context);  
            }  
            else context.Fail();    // 授权失败  
        }  

        /// <summary>  
        /// 验证管道,也就是验证核心代码  
        /// </summary>  
        /// <param name="context"></param>  
        /// <param name="httpContext"></param>  
        /// <returns></returns>  
        public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)  
        {  
            // 检查权限,如果方法是异步的就不用 Task.FromResult 包裹,直接使用 async/await 即可  
            return Task.FromResult(true);  
        }  
    }  
}  

15.5.3 客户端部分

客户端每次请求需将 accessTokenrefreshToken 放到请求报文头中传送到服务端,格式为:

Authorization: Bearer 你的token  
X-Authorization: Bearer 你的刷新token  

特别注意AuthorizationX-Authorization 都必须添加 Bearer 前缀。

Furion 框架提供了 vue/react/angular 客户端请求参考代码:https://gitee.com/dotnetchina/Furion/tree/v4/clients

小建议建议使用自动生成 Vue/React/Angular 代理方式:5.6 Vue/React/Angular 接口代理

其他补充在正常开发中,refreshToken 无需每次请求携带,而是 accessToken 即将过期之后携带即可。可以在客户端自行判断 accessToken 是否即将过期。

如果 Token 过期,那么 Furion 将自动根据有效期内的 refreshToken 自动生成新的 AccessToken,并在 响应报文头 中返回,如:

access-token: 新的token  
x-access-token: 新的刷新token  

存储新的 Token前端需要获取 响应报文头 新的 token 和刷新 token 替换之前在客户处存储旧的 token 和刷新 token。

15.6 获取 Jwt 存储的信息

// 获取 `Jwt` 存储的信息  
var userId = App.User?.FindFirstValue("键");  

注意引入 System.Security.Claims 命名空间

获取不到 Token 信息说明请确保 .AddJwt 服务已注册且启用了 全局授权 或该接口(方法)贴有 [Authorize] 特性。

15.7 前端解密 JWT 信息

通常在用户登录成功后我们会将 JWT Token 存储到浏览器中,这时候就需要在浏览器端解析 token 里面存储的信息,可以通过调用下面方法实现:

  • TypeScript 版本
/**  
 * 解密 JWT token 的信息  
 * @param token jwt token 字符串  
 * @returns <any>object  
 */  
function decryptJWT(token: string): any {  
  token = token.replace(/_/g, "/").replace(/-/g, "+");  
  var json = decodeURIComponent(escape(window.atob(token.split(".")[1])));  
  return JSON.parse(json);  
}  

  • JavaScript 版本
/**  
 * 解密 JWT token 的信息  
 * @param token jwt token 字符串  
 * @returns <any>object  
 */  
function decryptJWT(token) {  
  token = token.replace(/_/g, "/").replace(/-/g, "+");  
  var json = decodeURIComponent(escape(window.atob(token.split(".")[1])));  
  return JSON.parse(json);  
}  

这样就可以把后端放在 token 里面的信息解析出来了。

小知识可以在解密之后读取 过期时间 exp 来解决请求时是否需要带刷新 Token,比如即将过期前 5 分钟。

15.8 Jwt 身份验证过程监听

有时候我们希望能够自定义或者监听 Jwt 验证过程,比如验证失败后在 Response 中添加 Headers,或者对接第三方验证时要求提供 apikey 等方式,这时候就用到了自定义功能。

// 注册 JWT 授权  
services.AddJwt<AuthHandler>(jwtBearerConfigure: options =>  
{  
    // 实现 JWT 身份验证过程控制  
    options.Events = new JwtBearerEvents  
    {  
        // 添加额外 Token 读取处理  
        // 可以在这里实现任何方式的读取 Token,然后设置给 context.Token 即可  
        OnMessageReceived = context =>  
        {  
            return Task.CompletedTask;  
        },  
        // Token 验证通过处理  
        OnTokenValidated = context =>  
        {  
            return Task.CompletedTask;  
        },  
        // Token 验证失败处理  
        OnAuthenticationFailed = context =>  
        {  
            return Task.CompletedTask;  
        },  
        // 客户端未提供 Token 或 Token 格式不正确处理  
        OnChallenge = context =>  
        {  
            return Task.CompletedTask;  
        }  
    };  
});  

15.8.1 实现 Url 参数验证 Token

正常情况下,JWT 都是通过请求头的 Authorization 设置,我们可以通过下列代码实现支持 Url 设置 Token,如:

// 实现 JWT 身份验证过程控制  
services.AddJwt<AuthHandler>(jwtBearerConfigure: options =>  
{  
    // 实现 JWT 身份验证过程控制  
    options.Events = new JwtBearerEvents  
    {  
        // 添加读取 Token 的方式  
        OnMessageReceived = context =>  
        {  
            var httpContext = context.HttpContext;  

            // 判断请求是否包含 Authorization 参数,如果有就设置给 Token  
            if (httpContext.Request.Query.ContainsKey("Authorization"))  
            {  
                var token = httpContext.Request.Query["Authorization"];  

                // 设置 Token  
                context.Token = token;  
            }  

            return Task.CompletedTask;  
        }  
    };  
});  

这样就可以通过:https://www.xxxx.com?Authorization=你的Token 访问了。

15.9 关于 Blazor + WebAPI 混合授权

一些应用使用了 Blazor + WebAPI 模板后并启用全局授权,可能会遇到 401/403 授权失败的提示,这时只需要在启动层 YourProject.Web.Entry 下的 Pages/_Host.cshtml 顶部添加以下代码即可:

@using Microsoft.AspNetCore.Authorization  
@attribute [AllowAnonymous]  

15.10 启用全局授权导致重复检查问题

在一些复杂的应用给系统中,可能存在多种鉴权方式,如 JWTBasic,通常我们会启用全局鉴权,如:

services.AddJwt<JwtHandler>(enableGlobalAuthorize:true);  

这时候如果局部授权贴了 [Authorize] 特性,就会导致重复进入 JwtHandler 的情况,这时只需要调整全局授权问题即可,如:

// 1. 移除 enableGlobalAuthorize:true  
services.AddJwt<JwtHandler>();    

// 2. 添加 .RequireAuthorization()  
// 如果使用的是 WebApplication,那么直接使用 app.MapControllers().RequireAuthorization();  
app.UseEndpoints(endpoints =>  
{  
    endpoints.MapControllers().RequireAuthorization();  
});  

15.11 反馈与建议

与我们交流给 Furion 提 Issue


了解更多想了解更多 鉴权授权 知识可查阅 ASP.NET Core - 安全和标识 章节。